package edu.uj.petri.commands;

import edu.uj.util.Observed;
import java.util.ArrayList;
import edu.uj.util.ObserverList;
import edu.uj.util.Observer;

/**
 * "runtime environment" for Commands, allows their execution, undoing and
 * redoing.
 * Notifies observers of every undo, redo and execute.
 * @author Piotrek
 */
public class CommandStack implements Observed {
    
    /** list of already executed (and perhaps also undone) commands */
    private ArrayList<Command> commands;

    /**
     * points last executed command in {@link commands}. Decremented when undoing and incremented
     * when redoing or executing. Valid values: from -1 to commands.size()-1.
     */
    private int position;

    /** holds observers to be notified of events */
    private ObserverList observerList;

    /** Observed event code */
    public static final int UNDO = 3123;
    /* Observed event code  */
    public static final int REDO = 3124;
    /** Observed event code  */
    public static final int EXECUTE = 3125;

    public CommandStack() {
        commands = new ArrayList<Command>();
        position = -1;
        observerList = new ObserverList();
    }

    /**
     * returns true when it is possible to perform a single undo.
     * Returns false when all commands have been undone or there are no
     * commands executed.
     */
    public boolean undoPossible() {
        return !commands.isEmpty() && position >= 0;
    }

    /**
     * undoes one single command.
     * @throws Exception when it is impossible to undo
     */
    public void undo() throws CommandException {
        if (!undoPossible())
            throw new CommandException("cannot undo");
        
        commands.get(position).undo();
        --position;
        observerList.notifyAllSubscribers(UNDO);
    }

    /**
     * returns true when it is possible to perform a single redo.
     * Returns false when called just after execute or when there were no
     * undos
     */
    public boolean redoPossible() {
        return !commands.isEmpty() && position < commands.size()-1;
    }

    /**
     * redoes one single command. Further redos are impossible until an undo
     * is performed.
     * @throws Exception when it is impossible to redo
     */
    public void redo() throws CommandException {
        if (!redoPossible())
            throw new CommandException("cannot redo");
        
        ++position;
        commands.get(position).execute();
        observerList.notifyAllSubscribers(REDO);
    }

    /**
     * executes given command. The command is later possible to be undone.
     *
     * <p>Important: some commands may be combined with another ones. As result,
     * single undo undoes effects of more than one command.</p>
     */
    public void execute(Command command) {
        command.execute();
        if (!command.isExecuteAndForget())
            addCommand(command);
        observerList.notifyAllSubscribers(EXECUTE);
    }
    
    /**
     * puts command on stack; combines it with current top if possible
     */
    protected void addCommand(Command command) {
        for(int i=commands.size()-1; i>position; --i) // remove all after position
            commands.remove(i);
        
        if (position >= 0 && commands.get(position).canBeCombined(command)) {
            commands.get(position).combine(command);
        } else {
            ++position;
            commands.add(command);
        }
    }
    
    /**
     * removes all commands
     */
    public void clear() {
        commands.clear();
        position = -1;
    }

    /**
     * returns names of commands possible to be undone, with first to be undone 
     * first
     */
    public String[] getAvailForUndo() {
        int availCount = position+1;
        String[] retval = new String[availCount];
        for (int i=0; i<availCount; ++i)
            retval[i] = commands.get(position-i).toString();
        return retval;
    }

    /**
     * returns names of commands possible to be redone, with first to be redone
     * first
     */
    public String[] getAvailForRedo() {
        int availCount= commands.size()-1-position;
        String[] retval = new String[availCount];
        for (int i=0; i<availCount; ++i)
            retval[i] = commands.get(position+1+i).toString();
        return retval;
    }

    /**
     * subscribes observer to this command stack and notifies of every undo,
     * redo and execute
     */
    public void subscribe(Observer o) {
        observerList.subscribe(o);
    }

    /** unsubscribes observer */
    public void unsubscribe(Observer o) {
        observerList.unsubscribe(o);
    }
}
